package com.hqd.ch03.v51.boot.context.properties;

import com.hqd.ch03.v51.beans.BeansException;
import com.hqd.ch03.v51.beans.converter.ConversionService;
import com.hqd.ch03.v51.beans.factory.config.BeanDefinition;
import com.hqd.ch03.v51.beans.factory.support.BeanDefinitionBuilder;
import com.hqd.ch03.v51.beans.property.PropertyEditorRegistry;
import com.hqd.ch03.v51.boot.context.properties.bind.*;
import com.hqd.ch03.v51.boot.context.properties.bind.handler.IgnoreErrorsBindHandler;
import com.hqd.ch03.v51.boot.context.properties.bind.handler.IgnoreTopLevelConverterNotFoundBindHandler;
import com.hqd.ch03.v51.boot.context.properties.bind.handler.NoUnboundElementsBindHandler;
import com.hqd.ch03.v51.boot.context.properties.source.ConfigurationPropertyName;
import com.hqd.ch03.v51.boot.context.properties.source.ConfigurationPropertySource;
import com.hqd.ch03.v51.boot.context.properties.source.ConfigurationPropertySources;
import com.hqd.ch03.v51.boot.context.properties.source.UnboundElementsSourceFilter;
import com.hqd.ch03.v51.context.ApplicationContext;
import com.hqd.ch03.v51.context.ApplicationContextAware;
import com.hqd.ch03.v51.context.ConfigurableApplicationContext;
import com.hqd.ch03.v51.context.validation.Validator;
import com.hqd.ch03.v51.core.annotation.MergedAnnotations;
import com.hqd.ch03.v51.core.env.PropertySources;
import com.hqd.ch03.v51.factory.BeanFactory;
import com.hqd.ch03.v51.registry.BeanDefinitionRegistry;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

class ConfigurationPropertiesBinder {

    private static final String BEAN_NAME = "org.springframework.boot.context.internalConfigurationPropertiesBinder";

    private static final String FACTORY_BEAN_NAME = "org.springframework.boot.context.internalConfigurationPropertiesBinderFactory";

    private static final String VALIDATOR_BEAN_NAME = EnableConfigurationProperties.VALIDATOR_BEAN_NAME;

    private final ApplicationContext applicationContext;

    private final PropertySources propertySources;

    private final Validator configurationPropertiesValidator;

    private boolean jsr303Present;

    private volatile Validator jsr303Validator;

    private volatile Binder binder;

    ConfigurationPropertiesBinder(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        this.propertySources = new PropertySourcesDeducer(applicationContext).getPropertySources();
        this.configurationPropertiesValidator = getConfigurationPropertiesValidator(applicationContext);
    }

    static void register(BeanDefinitionRegistry registry) {
        if (!registry.containsBeanDefinition(FACTORY_BEAN_NAME)) {
            BeanDefinition definition = BeanDefinitionBuilder
                    .rootBeanDefinition(ConfigurationPropertiesBinder.Factory.class).getBeanDefinition();
            definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
            registry.registerBeanDefinition(ConfigurationPropertiesBinder.FACTORY_BEAN_NAME, definition);
        }
        if (!registry.containsBeanDefinition(BEAN_NAME)) {
            BeanDefinition definition = BeanDefinitionBuilder
                    .rootBeanDefinition(ConfigurationPropertiesBinder.class,
                            () -> ((BeanFactory) registry)
                                    .getBean(FACTORY_BEAN_NAME, ConfigurationPropertiesBinder.Factory.class).create())
                    .getBeanDefinition();
            definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
            registry.registerBeanDefinition(ConfigurationPropertiesBinder.BEAN_NAME, definition);
        }
    }

    static ConfigurationPropertiesBinder get(BeanFactory beanFactory) {
        return beanFactory.getBean(BEAN_NAME, ConfigurationPropertiesBinder.class);
    }

    BindResult<?> bind(ConfigurationPropertiesBean propertiesBean) {
        Bindable<?> target = propertiesBean.asBindTarget();
        ConfigurationProperties annotation = propertiesBean.getAnnotation();
        BindHandler bindHandler = getBindHandler(target, annotation);
        return getBinder().bind(annotation.prefix(), target, bindHandler);
    }

    Object bindOrCreate(ConfigurationPropertiesBean propertiesBean) {
        Bindable<?> target = propertiesBean.asBindTarget();
        ConfigurationProperties annotation = propertiesBean.getAnnotation();
        BindHandler bindHandler = getBindHandler(target, annotation);
        return getBinder().bindOrCreate(annotation.prefix(), target, bindHandler);
    }

    private Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext) {
        if (applicationContext.containsBean(VALIDATOR_BEAN_NAME)) {
            return applicationContext.getBean(VALIDATOR_BEAN_NAME, Validator.class);
        }
        return null;
    }

    private <T> BindHandler getBindHandler(Bindable<T> target, ConfigurationProperties annotation) {
        List<Validator> validators = getValidators(target);
        BindHandler handler = getHandler();
        handler = new ConfigurationPropertiesBindHandler(handler);
        if (annotation.ignoreInvalidFields()) {
            handler = new IgnoreErrorsBindHandler(handler);
        }
        if (!annotation.ignoreUnknownFields()) {
            UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
            handler = new NoUnboundElementsBindHandler(handler, filter);
        }
        if (!validators.isEmpty()) {
            handler = null;
        }
        for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
            handler = advisor.apply(handler);
        }
        return handler;
    }

    private IgnoreTopLevelConverterNotFoundBindHandler getHandler() {
        BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.applicationContext);
        return (bound != null)
                ? new IgnoreTopLevelConverterNotFoundBindHandler(new BoundPropertiesTrackingBindHandler(bound::add))
                : new IgnoreTopLevelConverterNotFoundBindHandler();
    }

    private List<Validator> getValidators(Bindable<?> target) {
        List<Validator> validators = new ArrayList<>(3);
        if (this.configurationPropertiesValidator != null) {
            validators.add(this.configurationPropertiesValidator);
        }
        if (target.getValue() != null && target.getValue().get() instanceof Validator) {
            validators.add((Validator) target.getValue().get());
        }
        return validators;
    }

    private List<ConfigurationPropertiesBindHandlerAdvisor> getBindHandlerAdvisors() {
        return this.applicationContext.getBeanProvider(ConfigurationPropertiesBindHandlerAdvisor.class).orderedStream()
                .collect(Collectors.toList());
    }

    private Binder getBinder() {
        if (this.binder == null) {
            this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(),
                    getConversionServices(), getPropertyEditorInitializer(), null,
                    ConfigurationPropertiesBindConstructorProvider.INSTANCE);
        }
        return this.binder;
    }

    private Iterable<ConfigurationPropertySource> getConfigurationPropertySources() {
        return ConfigurationPropertySources.from(this.propertySources);
    }

    private PropertySourcesPlaceholdersResolver getPropertySourcesPlaceholdersResolver() {
        return new PropertySourcesPlaceholdersResolver(this.propertySources);
    }

    private List<ConversionService> getConversionServices() {
        return new ConversionServiceDeducer(this.applicationContext).getConversionServices();
    }

    private Consumer<PropertyEditorRegistry> getPropertyEditorInitializer() {
        if (this.applicationContext instanceof ConfigurableApplicationContext) {
            return ((ConfigurableApplicationContext) this.applicationContext).getBeanFactory()::copyRegisteredEditorsTo;
        }
        return null;
    }

    /**
     * Factory bean used to create the {@link ConfigurationPropertiesBinder}. The bean
     * needs to be {@link ApplicationContextAware} since we can't directly inject an
     * {@link ApplicationContext} into the constructor without causing eager
     * {@link FactoryBean} initialization.
     */
    static class Factory implements ApplicationContextAware {

        private ApplicationContext applicationContext;

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }

        ConfigurationPropertiesBinder create() {
            return new ConfigurationPropertiesBinder(this.applicationContext);
        }

    }

    /**
     * {@link BindHandler} to deal with
     * {@link ConfigurationProperties @ConfigurationProperties} concerns.
     */
    private static class ConfigurationPropertiesBindHandler extends AbstractBindHandler {

        ConfigurationPropertiesBindHandler(BindHandler handler) {
            super(handler);
        }

        @Override
        public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
            return isConfigurationProperties(target.getType().resolve())
                    ? target.withBindRestrictions(Bindable.BindRestriction.NO_DIRECT_PROPERTY) : target;
        }

        private boolean isConfigurationProperties(Class<?> target) {
            return target != null && MergedAnnotations.from(target).isPresent(ConfigurationProperties.class);
        }

    }

}
